package upm.pb4agent.team2.environment;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import es.decoroso.communicationAgent.bean.Message;
import es.decoroso.communicationAgent.bean.Rower;
import es.decoroso.communicationAgent.bean.Vector2;
import es.decoroso.fakeAgent.behaviours.NotifyGraphicLayer;

import jade.content.AgentAction;
import jade.content.Concept;
import jade.content.ContentElement;
import jade.content.lang.Codec.CodecException;
import jade.content.onto.OntologyException;
import jade.content.onto.basic.Action;
import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.SimpleBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import upm.pb4agent.ontology.DenyRow;
import upm.pb4agent.ontology.PickSeat;
import upm.pb4agent.ontology.RealRow;
import upm.pb4agent.team2.Constants;
import upm.pb4agent.team2.Team2BaseAgent;
import upm.pb4agent.team2.internalOntology.InternalEnteringDock;
import upm.pb4agent.team2.internalOntology.InternalLeavingDock;
import upm.pb4agent.team2.internalOntology.PickSeatForward;
import upm.pb4agent.team2.internalOntology.WatcherReference;


/**
 * Boat Agent
 * 
 * @version 1.0
 * @author Alejandro Bustamante
 */
public class Boat extends Team2BaseAgent 
{
	private static final long serialVersionUID = -1898469562236227802L;

	private AID myWatcherAID;
	
	private AID refereeAID;
	
	private int x = 0;
	private int y = 0;
	
	private int circuit_length;
	private int boarding_extension;
	
	private HashMap<String, AID> seatHash = new HashMap<String, AID>();
	private HashMap<AID, Integer> rowersInBoatHash = new HashMap<AID, Integer>();
	
	
	// --------------------------------------------------------------------
	// --------------- GUI INTEGRATION CODE STARTS HERE -------------------
	// --------------------------------------------------------------------
	
	////////////////////////////////////////////////
	////  ADD ALL THIS PARAMS 					////
	////////////////////////////////////////////////
	/** Actual position of the boat */
	Vector2 position = new Vector2(0f, 0f);
	
	/** Actual orientation of the boat */
	Vector2 orientation = new Vector2(0f, 0f);
	
	/** Group number */ // Put your group number here
	int groupNumber = 0;
	
	/** Rowers of the boat */ // You have to update this variable
	List<Rower> rowers = new LinkedList<Rower>();
	
	// Params used to calculate the row 
	float timer = 3000f;
	float posX, posY;
	float rowX, rowY;
	
	/** Id of the communication agent */
	private AID comunicationAgent;

	// --------------------------------------------------------------------
	// --------------- GUI INTEGRATION CODE ENDS HERE ---------------------
	// --------------------------------------------------------------------
	
	protected void setup() 
	{
		super.setup();
		
		Object[] args = getArguments();
		if (args != null) 
		{
			groupNumber = Integer.parseInt((String)args[0]);
		}
		
		doWait(2000); // Wait for the communication service to be ready
		
		// SET THE START POSITION
		setStartPosition();

		// FIND THE COMMUNICATION AGENT
		this.comunicationAgent = locateService(this, "COMMUNICATION"); // ADD THIS  LINE

		// ADD THE BEHAVIOUR
		addBehaviour(new NotifyGraphicLayer(this, comunicationAgent)); // ADD THIS LINE
		
		try 
		{
			circuit_length = Integer.parseInt(getPropertyFromConfigFile("circuit_length"));
			boarding_extension = Integer.parseInt(getPropertyFromConfigFile("boarding_extension"));
		}
		catch (NumberFormatException e) 
		{
			e.printStackTrace();
		} 
		catch (FileNotFoundException e) 
		{
			e.printStackTrace();
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
		}
		
		refereeAID = findAgentOnDF(this, Constants.REFEREE);

		addBehaviour(new MessageListener(this));
	}
	
	// -------------------------------------------
	// --------------- METHODS -------------------
	// -------------------------------------------

	private void internalAddRower(AID rowerAID, String seatID)
	{
		if(!rowersInBoatHash.containsKey(rowerAID))
		{
			int seat = Integer.parseInt(seatID);
			rowersInBoatHash.put(rowerAID, seat);
			addRower(rowersInBoatHash.size(), seat);
		}
	}

	// -------------------------------------------
	// --------------- CLASSES -------------------
	// -------------------------------------------
	
	class MessageListener extends SimpleBehaviour 
	{
		private static final long serialVersionUID = 5222150218136782430L;
		
		//flags used to control messages sent to the Referee
		private boolean enteringDockBCalled = false;
		private boolean enteringDockCCalled = false;
		private boolean enteringDockDCalled = false;
		
		private boolean leavingDockBCalled = false;
		private boolean leavingDockCCalled = false;
		private boolean leavingDockDCalled = false;
		
		private boolean finishedRaceCalled = false;

		public MessageListener(Agent agent) 
		{
			super(agent);
		}

		@Override
		public void action() 
		{
			ACLMessage aclMessage = receive(MessageTemplate.and(MessageTemplate.MatchLanguage(codec.getName()), MessageTemplate.or(MessageTemplate.MatchOntology(ontology.getName()), MessageTemplate.MatchOntology(internalOntology.getName()))));

			printReceivedProtocol(aclMessage);
			
			if (aclMessage != null) 
			{
				if (aclMessage.getPerformative() == ACLMessage.INFORM) 
				{
					try 
					{
						ContentElement contentElement = null;
						contentElement = getContentManager().extractContent(aclMessage);
						
						if (contentElement instanceof AgentAction)
						{
							Action action = (Action) contentElement;
							Concept concept = action.getAction();
							
							if (concept instanceof WatcherReference)
							{
								// keep a reference to this boat's watcher's AID to pass it the accepted PickSeats - so he can keep track of the time it takes for agents to switch seats
								myWatcherAID = ((WatcherReference) concept).getWatcherAID();
							}
							
							else if (concept instanceof PickSeat)
							{
								String seatID = ((PickSeat) concept).getSeatID().getSeatID();
								if (seatID != null)
								{
									if(!seatHash.containsKey(seatID))
									{
										//allow the change of seat
										printOut("Allowed PickSeat request for seatID = "+seatID+" received from "+aclMessage.getSender().getLocalName());
										
										//reply to the rowers request, accepting the seat change
										sendMessage(aclMessage.getSender(), ACLMessage.INFORM, new Action(aclMessage.getSender(), concept), PickSeat.class.getSimpleName().toString());
										
										//pass the PickSeat to the Watcher, to keep track of time of change of seat
										if (myWatcherAID != null)
										{
											PickSeatForward forward = new PickSeatForward();
															forward.setSeatID(seatID);
															forward.setRowerAID(aclMessage.getSender());
											
											sendInternalMessage(myWatcherAID, ACLMessage.INFORM, new Action(myWatcherAID, forward), PickSeatForward.class.getSimpleName().toString());
										}
										
										//remove old seat if sender had a previous seat and add new seat to the hash
										
										Iterator itr = (Iterator) seatHash.entrySet().iterator();
									    while (itr.hasNext()) 
									    {
									    	Map.Entry pairs = (Map.Entry) itr.next();
									    	
									    	if(pairs.getValue() == aclMessage.getSender())
									    	{
									    		seatHash.remove(pairs.getKey());
									    	}
									        itr.remove();
									    }
									    
									    seatHash.put(seatID, aclMessage.getSender());
									    
									    internalAddRower(aclMessage.getSender(), seatID);
									}
									else
									{
										//seat is taken already, so deny the rowers request
										printOut("Refused PickSeat request for seatID = "+seatID+" received from "+aclMessage.getSender().getLocalName()+" - this seat is already taken by "+seatHash.get(seatID).getLocalName());
										sendMessage(aclMessage.getSender(), ACLMessage.REFUSE, new Action(aclMessage.getSender(), concept), PickSeat.class.getSimpleName().toString());
									}
								}
							}
							
							else if (concept instanceof RealRow)
							{
								RealRow realRow = ((RealRow) concept);
								
								//printOut("Received a RealRow instance with values x = "+realRow.getX()+" and y = "+realRow.getY()+" from attempt to row on row on oarID: "+realRow.getOarID()+" from " + aclMessage.getSender().getLocalName());

								printOut("		Accepted row on oar "+realRow.getOarID());
								calculateNewXY(realRow);
							}
						}
					} 
					catch (CodecException e) 
					{
						e.printStackTrace();
					} 
					catch (OntologyException oe) 
					{
						oe.printStackTrace();
					}
				} 
				if (aclMessage.getPerformative() == ACLMessage.AGREE) 
				{
					printOut("received AGREE");
				} 
				else if (aclMessage.getPerformative() == ACLMessage.REFUSE) 
				{
					try 
					{
						ContentElement contentElement = null;
						contentElement = getContentManager().extractContent(aclMessage);
						
						if (contentElement instanceof AgentAction)
						{
							Action action = (Action) contentElement;
							Concept concept = action.getAction();
							
							if (concept instanceof DenyRow)
							{
								DenyRow denyRow = ((DenyRow) concept);
								printOut("Received a DenyRow instance from attempt to row on oarID: "+denyRow.getOarID()+" from " + aclMessage.getSender().getLocalName());
							}
						}
					}
					catch (CodecException e) 
					{
						e.printStackTrace();
					} 
					catch (OntologyException oe) 
					{
						oe.printStackTrace();
					}
				}
				else if (aclMessage.getPerformative() == ACLMessage.NOT_UNDERSTOOD) 
				{
					// Message format is wrong
					printOut("received NOT_UNDERSTOOD");
				}
			} 
			else 
			{
				// No message yet
				block();
			}
		}
		
		private void calculateNewXY(RealRow realRow)
		{
			x = x + realRow.getX();
			y = y + realRow.getY();
			
			printOut("		New x = "+x+" new y = "+y);
			
			toRow(x, y);
			
			if (((circuit_length / 4 - boarding_extension/2) <= y) && !enteringDockBCalled)
			{
				//Boat is entering dock B
				InternalEnteringDock internalEnteringDock = new InternalEnteringDock();
				internalEnteringDock.setBoatAID(myAgent.getAID());
				internalEnteringDock.setDockName(Constants.DOCK_B_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalEnteringDock), InternalEnteringDock.class.getSimpleName().toString());
				
				enteringDockBCalled = true;
			}
			else if ((y >= (circuit_length / 4 + boarding_extension/2)) && !leavingDockBCalled)
			{
				//Boat is leaving dock B
				InternalLeavingDock internalLeavingDock = new InternalLeavingDock();
				internalLeavingDock.setBoatAID(myAgent.getAID());
				internalLeavingDock.setDockName(Constants.DOCK_B_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalLeavingDock), InternalLeavingDock.class.getSimpleName().toString());
				
				leavingDockBCalled = true;
			}

			else if (((circuit_length / 2 - boarding_extension/2) <= y) && !enteringDockCCalled)
			{
				//Boat is entering dock C
				InternalEnteringDock internalEnteringDock = new InternalEnteringDock();
				internalEnteringDock.setBoatAID(myAgent.getAID());
				internalEnteringDock.setDockName(Constants.DOCK_C_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalEnteringDock), InternalEnteringDock.class.getSimpleName().toString());
				
				enteringDockCCalled = true;
			}
			else if ((y >= (circuit_length / 2 + boarding_extension/2)) && !leavingDockCCalled)
			{
				//Boat is leaving dock C
				InternalLeavingDock internalLeavingDock = new InternalLeavingDock();
				internalLeavingDock.setBoatAID(myAgent.getAID());
				internalLeavingDock.setDockName(Constants.DOCK_C_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalLeavingDock), InternalLeavingDock.class.getSimpleName().toString());
				
				leavingDockCCalled = true;
			}

			else if ((((circuit_length * 3 / 4) - boarding_extension/2) <= y) && !enteringDockDCalled)
			{
				//Boat is entering dock D
				InternalEnteringDock internalEnteringDock = new InternalEnteringDock();
				internalEnteringDock.setBoatAID(myAgent.getAID());
				internalEnteringDock.setDockName(Constants.DOCK_D_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalEnteringDock), InternalEnteringDock.class.getSimpleName().toString());
				
				enteringDockDCalled = true;
			}
			else if ((y >= ((circuit_length * 3 / 4) + boarding_extension/2)) && !leavingDockDCalled)
			{
				//Boat is leaving dock D
				InternalLeavingDock internalLeavingDock = new InternalLeavingDock();
				internalLeavingDock.setBoatAID(myAgent.getAID());
				internalLeavingDock.setDockName(Constants.DOCK_D_ID);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalLeavingDock), InternalLeavingDock.class.getSimpleName().toString());
				
				leavingDockDCalled = true;
			}
			
			else if(y >= circuit_length && !finishedRaceCalled)
			{
				//Boat has finished racing
				InternalEnteringDock internalEnteringDock = new InternalEnteringDock();
				internalEnteringDock.setBoatAID(myAgent.getAID());
				internalEnteringDock.setDockName(Constants.FINISH_LINE);
				
				sendInternalMessage(refereeAID, ACLMessage.INFORM, new Action(refereeAID, internalEnteringDock), InternalEnteringDock.class.getSimpleName().toString());
				
				finishedRaceCalled = true;
			}
		}

		@Override
		public boolean done() 
		{
			return finishedRaceCalled;
		}

	}
	
	
	// --------------------------------------------------------------------
	// --------------- GUI INTEGRATION CODE STARTS HERE -------------------
	// --------------------------------------------------------------------
	
	///////////////////////////////////////////////////////
	// ADD THE CODE FROM HERE TO THE END OF THE DOCUMENT //
	///////////////////////////////////////////////////////
	/**
	 * Call this method after set the group number
	 * 
	 * This methods sets the start position of the boat
	 */
	private void setStartPosition() {
		position.x = groupNumber*25;

	}
	
	private void addRower(int rowerNumber, int seatNumber) 
	{
        for (Rower rower : rowers) 
        {
                // Check if it already exists in the list
                if (rower.getNumber() == rowerNumber) 
                {
                        rowers.remove(rower);
                        break;
                }
        }

        rowers.add(new Rower(rowerNumber, seatNumber));
	}
	
	/**
	 * Start a row
	 * 
	 * This method should be called when a new row it's been received.
	 * 
	 * @param x X Value of the row
	 * @param y Y Value of the row
	 */
	public void toRow(float x, float y) {
		posX = position.x;
		posY = position.y;

		if (timer < 3000) { // row not finished
			float restX =  orientation.x - orientation.x * timer / 3000.0f;
			float restY =  orientation.y - orientation.y * timer / 3000.0f;

			orientation.x = restX + x;
			orientation.y = restY + y;
		} else {
			orientation.x = x;
			orientation.y = y;
		}

		timer = 0;
	}

	
	/**
	 * Update the position of the boat
	 */
	private void updateRow() {
		if (timer <= 3000) {
			position.x = posX + (orientation.x * timer / 3000.0f);
			position.y = posY + (orientation.y * timer / 3000.0f);
		}
	}

	/**
	 * This method is called from NotifyGraphicLayer
	 * 
	 * This method updates the row force and send the information
	 * to the graphic layer
	 * 
	 * @return Message with the actual position of the system
	 */
	public Message createMessage() {
		timer = timer + 50;
		updateRow();
		Message m = new Message(groupNumber, orientation, position, rowers);
		return m;
	}

	/**
	 * This method search for an agent that offers a service we are searching
	 * for
	 * 
	 * @param agentType Type of agent you want to locate
	 * 
	 * @return AID of identifyed agent or null if not found
	 */
	private AID locateService(Agent agent, String agentType) {

		AID result = null;

		// Create agent description
		DFAgentDescription dfd = new DFAgentDescription();
		ServiceDescription sd = new ServiceDescription();
		sd.setType(agentType);
		dfd.addServices(sd);

		// Search requested type agents
		DFAgentDescription[] res = new DFAgentDescription[1];
		try {
			res = DFService.search(agent, dfd);
			if (res.length > 0) {
				result = res[0].getName();
			} else {
				// Not agent found
				return null;
			}
		} catch (FIPAException ex) {
			// Not agent found exception
			ex.printStackTrace();
		}
		return result;
	}
	
	// --------------------------------------------------------------------
	// --------------- GUI INTEGRATION CODE ENDS HERE ---------------------
	// --------------------------------------------------------------------

}
